---
title: Business rules
summary: Business rules enforced during operation processing.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-09-22
---

# Business rules

This host enforces 50 business rules across its six services, spanning input validation, authorization-cache and directory-lookup behavior in `SecurityService`, preference/session persistence rules, imaging queue placement, fax dispatch sequencing, and the host-wide unhandled-exception logging rule that applies to every operation.

---

## BR-WCF-001 Fault logging on every unhandled exception

| Field | Value |
| --- | --- |
| Statement | Every unhandled exception on a decorated service is logged as an `ApplicationExceptionDetail` under the fixed application name `WCF` and is then allowed to continue to the standard fault path. |
| Trigger | Any unhandled exception in any operation. |
| Enforcement | `ErrorLogErrorHandler.HandleError` installed by `LogErrorBehaviorAttribute` — `Extensions/ErrorLogErrorHandler.cs`, `Extensions/LogErrorBehaviorAttribute.cs`. |
| Origin | service |
| Reached from | All operations — [operation catalog](operations.md#operation-catalog) |
| Outcome | Record written through `LogService`; `HandleError` returns `false`, so the caller receives the framework-generated fault carrying full exception detail. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Extensions/ErrorLogErrorHandler.cs` |

---

## BR-WCF-002 Per-request configuration and connection resolution

| Field | Value |
| --- | --- |
| Statement | Configuration and database connections are resolved per request from the caller's operation context, never from a static field or a connection string. |
| Trigger | Service instantiation. |
| Enforcement | `new WcfEnvironmentService(OperationContext.Current.RequestContext)` plus `DbConnectionManager.Get*ConnectionString` in each constructor. |
| Origin | service |
| Reached from | All operations except `FileService` — [operation catalog](operations.md#operation-catalog) |
| Outcome | Service instance bound to that request's resolved settings; a missing `OperationContext` makes construction impossible. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-003 ReadFile returns a fixed-length buffer

| Field | Value |
| --- | --- |
| Statement | `ReadFile` returns exactly `byteLength` bytes regardless of the real file length, and does not report how many bytes were actually read. |
| Trigger | `ReadFile` invocation. |
| Enforcement | `FileService.ReadFile` — `Services/FileService.cs`. |
| Origin | service |
| Reached from | [`ReadFile`](contracts/file-service.md#readfile) |
| Outcome | Buffer of `byteLength` bytes, zero-padded when the file is shorter than requested. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/FileService.cs` |

---

## BR-WCF-004 File operations trust the caller-supplied path

| Field | Value |
| --- | --- |
| Statement | File operations act directly on the caller-supplied path with no validation, canonicalization, containment check, or authorization. |
| Trigger | Any `FileService` operation, or an internal `WriteFile`/`DeleteFile` call from `FaxService` or `ImagingService`. |
| Enforcement | `FileService` — `Services/FileService.cs`. |
| Origin | service |
| Reached from | [`ReadFile`](contracts/file-service.md#readfile); [`ReadEntireFile`](contracts/file-service.md#readentirefile); [`WriteFile`](contracts/file-service.md#writefile); [`DeleteFile`](contracts/file-service.md#deletefile); [`Length`](contracts/file-service.md#length); [`SendFax`](contracts/fax-service.md#sendfax); [`QueueImage`](contracts/imaging-service.md#queueimage) |
| Outcome | Operation succeeds for any path the app-pool identity can reach; otherwise a filesystem exception is logged and returned as a detailed fault. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/FileService.cs` |

---

## BR-WCF-005 Preference keys are case-normalized

| Field | Value |
| --- | --- |
| Statement | Preference lookup keys are case-normalized: `user` and `applicationName` are lower-cased and a blank `pageName` is treated as the empty string. |
| Trigger | `GetPreferences`, `SavePreferences`. |
| Enforcement | `PreferenceService.QueryPrefs` — `Services/PreferenceService.cs`. |
| Origin | service |
| Reached from | [`GetPreferences`](contracts/preference-service.md#getpreferences); [`SavePreferences`](contracts/preference-service.md#savepreferences) |
| Outcome | Case-insensitive match against stored preferences. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-006 Missing preference returns an empty dictionary

| Field | Value |
| --- | --- |
| Statement | A missing preference record yields an empty dictionary rather than a null or a fault. |
| Trigger | `GetPreferences`. |
| Enforcement | `PreferenceService.GetPreferences`. |
| Origin | service |
| Reached from | [`GetPreferences`](contracts/preference-service.md#getpreferences) |
| Outcome | Empty `Dictionary<string, string>`. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-007 SavePreferences requires a current user

| Field | Value |
| --- | --- |
| Statement | `currentUser` must be supplied on every preference save.* |
| Trigger | `SavePreferences`. |
| Enforcement | `Enforce.ArgumentNotNullOrEmpty(currentUser)`. |
| Origin | inherited |
| Reached from | [`SavePreferences`](contracts/preference-service.md#savepreferences) |
| Outcome | `ArgumentNullException` before any database work, logged and surfaced as a detailed fault. |
| Source | `SAIF.Framework.Core (`Enforce`)` |

---

## BR-WCF-008 New preference set is inserted

| Field | Value |
| --- | --- |
| Statement | A non-empty preference set with no existing record is inserted, with a blank page name stored as null. |
| Trigger | `SavePreferences`. |
| Enforcement | `PreferenceService.AddPreference`. |
| Origin | service |
| Reached from | [`SavePreferences`](contracts/preference-service.md#savepreferences) |
| Outcome | New `Preference` row written inside the transaction. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-009 Emptied preference set is deleted

| Field | Value |
| --- | --- |
| Statement | An empty preference set against an existing record deletes that record. |
| Trigger | `SavePreferences`. |
| Enforcement | `PreferenceService.DeletePreference`. |
| Origin | service |
| Reached from | [`SavePreferences`](contracts/preference-service.md#savepreferences) |
| Outcome | `Preference` row deleted inside the transaction. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-010 Existing preference set is updated in place

| Field | Value |
| --- | --- |
| Statement | A non-empty preference set against an existing record replaces its stored JSON in place. |
| Trigger | `SavePreferences`. |
| Enforcement | `PreferenceService.UpdatePreference`. |
| Origin | service |
| Reached from | [`SavePreferences`](contracts/preference-service.md#savepreferences) |
| Outcome | `Preference` row updated inside the transaction. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-011 Persisted changes stamp a local timestamp

| Field | Value |
| --- | --- |
| Statement | Every persisted change stamps the update timestamp from server local time (`DateTime.Now`), and preference writes additionally stamp the lower-cased `currentUser`. |
| Trigger | `SavePreferences`, `WebContextService.Save`. |
| Enforcement | `PreferenceService.PopulatePreference`; `WebContextService.Save`. |
| Origin | service |
| Reached from | [`SavePreferences`](contracts/preference-service.md#savepreferences); [`Save`](contracts/web-context-service.md#save) |
| Outcome | Audit fields populated; no UTC normalization. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-012 Unknown user has no default printer

| Field | Value |
| --- | --- |
| Statement | An unknown user has no default printer. |
| Trigger | `GetDefaultPrinter`. |
| Enforcement | `PreferenceService.GetDefaultPrinter`. |
| Origin | service |
| Reached from | [`GetDefaultPrinter`](contracts/preference-service.md#getdefaultprinter) |
| Outcome | `string.Empty` returned. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs` |

---

## BR-WCF-013 Default printer comes from an AD attribute

| Field | Value |
| --- | --- |
| Statement | The default printer is the AD attribute `extensionAttribute2` on the SAIF-domain principal. |
| Trigger | `GetDefaultPrinter`. |
| Enforcement | `SaifUserPrincipal.DefaultPrinter` — `Services/Security/SaifUserPrincipal.cs`. |
| Origin | service |
| Reached from | [`GetDefaultPrinter`](contracts/preference-service.md#getdefaultprinter) |
| Outcome | Attribute value, or `""` when unset. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SaifUserPrincipal.cs` |

---

## BR-WCF-014 Web context operations require identifying arguments

| Field | Value |
| --- | --- |
| Statement | Web-context operations require a non-empty application name and session id, and `Save` additionally requires a context.* |
| Trigger | `Get`, `Save`. |
| Enforcement | `Enforce` guards — `Services/WebContextService.cs`. |
| Origin | inherited |
| Reached from | [`Get`](contracts/web-context-service.md#get); [`Save`](contracts/web-context-service.md#save) |
| Outcome | `ArgumentNullException`, logged and surfaced as a detailed fault. |
| Source | `SAIF.Framework.Core (`Enforce`)` |

---

## BR-WCF-015 Context identity comes from a stored PID or a new GUID

| Field | Value |
| --- | --- |
| Statement | The context identity is taken from the stored `__SAIFPID` item when present; otherwise a new GUID is generated. |
| Trigger | `Get`, `CreateContext`. |
| Enforcement | `WebContextService.CreateContext`. |
| Origin | service |
| Reached from | [`Get`](contracts/web-context-service.md#get); [`CreateContext`](contracts/web-context-service.md#createcontext) |
| Outcome | Context carrying the persisted or newly-minted id. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/WebContextService.cs` |

---

## BR-WCF-016 Navigation keys are never returned in context

| Field | Value |
| --- | --- |
| Statement | The navigation keys `sourceApp`, `sourceView`, `targetApp`, `targetView` are never copied into the returned context. |
| Trigger | `Get`. |
| Enforcement | `WebContextService.Get` exclusion array. |
| Origin | service |
| Reached from | [`Get`](contracts/web-context-service.md#get) |
| Outcome | Those keys are dropped from the response, then deleted from storage regardless. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/WebContextService.cs` |

---

## BR-WCF-017 Web context rows are read once

| Field | Value |
| --- | --- |
| Statement | Web-context rows are read-once: `Get` deletes every row it read (including excluded keys and `__SAIFPID`), and `Save` deletes all existing rows before writing the new set. |
| Trigger | `Get`, `Save`. |
| Enforcement | `WebContextService.Get`, `.Save`, inside the unit of work. |
| Origin | service |
| Reached from | [`Get`](contracts/web-context-service.md#get); [`Save`](contracts/web-context-service.md#save) |
| Outcome | Rows removed within the transaction; a fault rolls the deletes back and the context survives. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/WebContextService.cs` |

---

## BR-WCF-018 External LDAP host falls back to a literal default

| Field | Value |
| --- | --- |
| Statement | When the external LDAP host cannot be resolved from configuration, the literal `ldap.external.saif.com` is used. |
| Trigger | `SecurityService` construction. |
| Enforcement | `SecurityService` constructor — `Services/Security/SecurityService.cs`. |
| Origin | service |
| Reached from | All `SecurityService` operations — [contract page](contracts/security-service.md) |
| Outcome | External authorization proceeds against the default host. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SecurityService.cs` |

---

## BR-WCF-019 Fax proxy requires a resolved service URL

| Field | Value |
| --- | --- |
| Statement | The Legacy.Fax endpoint address is resolved from configuration at construction time and must be non-blank. |
| Trigger | `FaxService` construction. |
| Enforcement | `Enforce.ArgumentNotNullOrWhiteSpace(serviceUrl)` in `FaxServiceProxy` — `Services/LegacyProxies/FaxServiceProxy.cs`. |
| Origin | service |
| Reached from | [`SendFax`](contracts/fax-service.md#sendfax) |
| Outcome | `ArgumentNullException` before any fax work. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/LegacyProxies/FaxServiceProxy.cs` |

---

## BR-WCF-020 SendFax requires a config and recipients

| Field | Value |
| --- | --- |
| Statement | A fax request must supply a config and a recipient collection.* |
| Trigger | `SendFax`. |
| Enforcement | `Enforce` guards — `Services/FaxService.cs`. |
| Origin | inherited |
| Reached from | [`SendFax`](contracts/fax-service.md#sendfax) |
| Outcome | `ArgumentNullException`, logged and surfaced as a detailed fault. |
| Source | `SAIF.Framework.Core (`Enforce`)` |

---

## BR-WCF-021 Fax document is staged then deleted

| Field | Value |
| --- | --- |
| Statement | The fax document is staged as a GUID-named file in the configured temp folder and deleted after the last recipient is attempted. |
| Trigger | `SendFax`. |
| Enforcement | `FaxService.SendFax`. |
| Origin | service |
| Reached from | [`SendFax`](contracts/fax-service.md#sendfax) |
| Outcome | Temp file created then removed; a mid-loop fault leaves it in place because the delete is not in a `finally`. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/FaxService.cs` |

---

## BR-WCF-022 One fax dispatched per recipient

| Field | Value |
| --- | --- |
| Statement | One fax is dispatched per recipient, sequentially, each carrying the same staged file path, recipient fax number and recipient name. |
| Trigger | `SendFax`. |
| Enforcement | `FaxService.SendFax` loop → `FaxServiceProxy.SendFax` → Legacy.Fax `Send`. |
| Origin | service |
| Reached from | [`SendFax`](contracts/fax-service.md#sendfax) |
| Outcome | One downstream SOAP call per recipient; no atomicity across the set. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/FaxService.cs` |

---

## BR-WCF-023 Required arguments are presence checked before work

| Field | Value |
| --- | --- |
| Statement | Required arguments are presence-checked before any work; the imaging guards cover file, extension and image config, and the authorization guards cover user id and role.* |
| Trigger | `QueueImage`, `IsAuthorized2`. |
| Enforcement | `Enforce` guards — `Services/ImagingService.cs`, `Services/Security/SecurityService.cs`. |
| Origin | inherited |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage); [`IsAuthorized`](contracts/security-service.md#isauthorized); [`IsAuthorized2`](contracts/security-service.md#isauthorized2) |
| Outcome | `ArgumentNullException`, logged and surfaced as a detailed fault. `IsAuthorized2`'s third guard re-tests `role` under the label `domain`, so no `domain` value is actually validated. |
| Source | `SAIF.Framework.Core (`Enforce`)` |

---

## BR-WCF-024 Image id reserved before any file write

| Field | Value |
| --- | --- |
| Statement | Every queued image reserves its identifier from the imaging database before any file is written.* |
| Trigger | `QueueImage`, `ReserveImageId`. |
| Enforcement | `ImagingService.ReserveImageId` → `unitOfWork.GetRepository<Image>().GetNextImageId()`. |
| Origin | inherited |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage); [`ReserveImageId`](contracts/imaging-service.md#reserveimageid) |
| Outcome | Monotonic image id*; consumed irrevocably even when a later step fails. |
| Source | `SAIF.Framework.Data.NHibernate (imaging repository)` |

---

## BR-WCF-025 Queued image files are named by a fresh GUID

| Field | Value |
| --- | --- |
| Statement | Queued image files are named with a fresh GUID, not with the reserved image id or the caller's file name. |
| Trigger | `QueueImage`. |
| Enforcement | `ImagingService.WriteFileWithIdx`. |
| Origin | service |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage) |
| Outcome | Document and `.idx` share one GUID stem. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/ImagingService.cs` |

---

## BR-WCF-026 Priority folder selection for queued images

| Field | Value |
| --- | --- |
| Statement | When priority folders are enabled, a system-generated image is written to the low-priority folder and any other image to the medium-priority folder; when disabled, both go to the import queue root. |
| Trigger | `QueueImage`. |
| Enforcement | `ImagingService.WriteFileWithIdx`, driven by `wcf.useobimportpriorityfolders` and `imageConfig.SystemGeneratedFlag`. |
| Origin | service |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage) |
| Outcome | Target directory selected per image. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/ImagingService.cs` |

---

## BR-WCF-027 Composed image paths are normalized

| Field | Value |
| --- | --- |
| Statement | Composed image paths are normalized: the extension is forced to a leading `.` and the directory to a trailing backslash. |
| Trigger | `QueueImage`. |
| Enforcement | `ImagingService.EnsureFullPath`. |
| Origin | service |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage) |
| Outcome | Well-formed path regardless of how the configured values are punctuated. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/ImagingService.cs` |

---

## BR-WCF-028 Every queued image gets an idx sidecar

| Field | Value |
| --- | --- |
| Statement | Every queued image is accompanied by an ASCII `.idx` sidecar produced from the image config and the reserved id. |
| Trigger | `QueueImage`. |
| Enforcement | `ImagingService.WriteFileWithIdx` → `imageConfig.ToIDXFileString(imageId, taskConfig)`. |
| Origin | service |
| Reached from | [`QueueImage`](contracts/imaging-service.md#queueimage) |
| Outcome | Sidecar written next to the document; a failure here leaves an unindexed document. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/ImagingService.cs (sidecar format inherited from SAIF.Services.WCF.Imaging*)` |

---

## BR-WCF-029 Authorization answers are cached per user role and domain

| Field | Value |
| --- | --- |
| Statement | Authorization answers are cached per user, role and domain for a configured number of seconds, with no sliding expiration. |
| Trigger | `IsAuthorized`, `IsAuthorized2`. |
| Enforcement | `AuthorizationCacheService.IsAuthorized`. |
| Origin | service |
| Reached from | [`IsAuthorized`](contracts/security-service.md#isauthorized); [`IsAuthorized2`](contracts/security-service.md#isauthorized2) |
| Outcome | Cached result reused within the TTL; with no `HttpContext` present the cache is bypassed entirely in both directions. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/AuthorizationCacheService.cs` |

---

## BR-WCF-030 Internal domain is tried before external domain

| Field | Value |
| --- | --- |
| Statement | Authorization tries the internal domain first by default and falls back to the external domain; `IsAuthorized2` lets the caller skip the internal attempt, and a true answer from either domain authorizes. |
| Trigger | `IsAuthorized`, `IsAuthorized2`. |
| Enforcement | `SecurityService.IsAuthorized2`, `.IsAuthorizedInternal`, `.IsAuthorizedExternal`. |
| Origin | service |
| Reached from | [`IsAuthorized`](contracts/security-service.md#isauthorized); [`IsAuthorized2`](contracts/security-service.md#isauthorized2) |
| Outcome | `true` on the first domain that grants the role, otherwise `false`. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SecurityService.cs` |

---

## BR-WCF-031 Disabled accounts are never authorized

| Field | Value |
| --- | --- |
| Statement | A disabled account is never authorized, in either domain, and a missing group or missing user answers `false` rather than faulting. |
| Trigger | `IsAuthorized`, `IsAuthorized2`. |
| Enforcement | `SecurityService.IsAuthorizedExternal` (`user.Enabled` check) and `.IsAuthorizedInternal` (`SecurityException` message inspection). |
| Origin | service |
| Reached from | [`IsAuthorized`](contracts/security-service.md#isauthorized); [`IsAuthorized2`](contracts/security-service.md#isauthorized2) |
| Outcome | `false` returned; any `SecurityException` other than "account disabled" or "user name or password is incorrect" is rethrown. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SecurityService.cs` |

---

## BR-WCF-032 Every security operation logs its request

| Field | Value |
| --- | --- |
| Statement | Every security operation writes its name and arguments to the IIS log, unconditionally. |
| Trigger | Any `Security` operation. |
| Enforcement | `SecurityService.LogRequestDetails`. |
| Origin | service |
| Reached from | All `SecurityService` operations — [contract page](contracts/security-service.md) |
| Outcome | Log line appended; failures are swallowed by an empty `catch`. The `LOG_REQUEST_DETAILS` setting is read but never consulted. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SecurityService.cs` |

---

## BR-WCF-033 Projected contacts never return null strings

| Field | Value |
| --- | --- |
| Statement | Projected contacts never return a null string: every string member is normalized to the empty string, and missing company, name, address and phone objects are replaced with empty instances. |
| Trigger | Any operation returning a user. |
| Enforcement | `ContactFactory.MakeNullStringsEmpty` and its `SetEmptyIfNull` overloads — `Services/Security/Users/ContactFactory.cs`. |
| Origin | service |
| Reached from | All `SecurityService` operations — [contract page](contracts/security-service.md) |
| Outcome | Fully populated contact graph with empty strings in place of nulls. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/ContactFactory.cs` |

---

## BR-WCF-034 Customer id resolves to the most recent user

| Field | Value |
| --- | --- |
| Statement | Resolving a user from a customer id selects the most recently logged-on user of the permitted type for that customer group. |
| Trigger | `GetExternalUserByCustomerId`, `GetInjuredWorkerUserByCustomerId`. |
| Enforcement | `UserFactory.GetCustomerUserId` — ordered by `LastLogonDate` descending. |
| Origin | service |
| Reached from | [`GetExternalUserByCustomerId`](contracts/security-service.md#getexternaluserbycustomerid); [`GetInjuredWorkerUserByCustomerId`](contracts/security-service.md#getinjuredworkeruserbycustomerid) |
| Outcome | Single user id, or null when no match. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-035 Missing group returns null not empty array

| Field | Value |
| --- | --- |
| Statement | A group-membership request for a group that does not exist returns null rather than an empty array. |
| Trigger | `GetUsersByGroup`. |
| Enforcement | `ADService.GetUsersByGroup`. |
| Origin | service |
| Reached from | [`GetUsersByGroup`](contracts/security-service.md#getusersbygroup) |
| Outcome | `null` returned to the caller. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/ADService.cs` |

---

## BR-WCF-036 Group membership read through a paged directory search

| Field | Value |
| --- | --- |
| Statement | Group membership is read through a paged directory search (page size 1000, no size limit) sorted by account name. |
| Trigger | `GetUsersByGroup`. |
| Enforcement | `ADService.GetMembership`. |
| Origin | service |
| Reached from | [`GetUsersByGroup`](contracts/security-service.md#getusersbygroup) |
| Outcome | Complete membership returned for large groups. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/ADService.cs` |

---

## BR-WCF-037 Nested groups expanded recursively without cycles

| Field | Value |
| --- | --- |
| Statement | Nested groups are expanded recursively when requested, and a group already visited is never expanded twice. |
| Trigger | `GetUsersByGroup`. |
| Enforcement | `ADService.GetMembershipRecursively` with its visited-group list. |
| Origin | service |
| Reached from | [`GetUsersByGroup`](contracts/security-service.md#getusersbygroup) |
| Outcome | Cycle-safe transitive membership. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/ADService.cs` |

---

## BR-WCF-038 Disabled accounts excluded from group membership

| Field | Value |
| --- | --- |
| Statement | Disabled accounts are excluded from group membership unless the caller explicitly asks for them. |
| Trigger | `GetUsersByGroup`. |
| Enforcement | `ADService.GetMembershipRecursively` `userAccountControl` bit test. |
| Origin | service |
| Reached from | [`GetUsersByGroup`](contracts/security-service.md#getusersbygroup) |
| Outcome | Disabled members omitted by default. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/ADService.cs` |

---

## BR-WCF-039 Group membership is deduplicated and sorted

| Field | Value |
| --- | --- |
| Statement | Returned group membership is de-duplicated and sorted ascending by account name. |
| Trigger | `GetUsersByGroup`. |
| Enforcement | `.Distinct().OrderBy(x => x).ToArray()` in `ADService.GetUsersByGroup`. |
| Origin | service |
| Reached from | [`GetUsersByGroup`](contracts/security-service.md#getusersbygroup) |
| Outcome | Unique, ordered array returned. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/ADService.cs` |

---

## BR-WCF-018a Unmatched user is returned as non-existent

| Field | Value |
| --- | --- |
| Statement | A user who exists in no source is returned as a non-existent user rather than as null or a fault. |
| Trigger | `GetUser`. |
| Enforcement | `UserFactory.GetEmptyUser`. |
| Origin | service |
| Reached from | [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | `User` with `Exists = false`, empty contact, all flags false. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-019a External user active and locked state derived from one flag

| Field | Value |
| --- | --- |
| Statement | An external user's active state is its authorized-user flag, and its locked state is that flag negated. |
| Trigger | `GetExternalUser` and its callers. |
| Enforcement | `UserFactory.TryGetExternalUser`. |
| Origin | service |
| Reached from | [`GetExternalUser`](contracts/security-service.md#getexternaluser); [`GetExternalUserByCustomerId`](contracts/security-service.md#getexternaluserbycustomerid); [`GetInjuredWorkerUserByCustomerId`](contracts/security-service.md#getinjuredworkeruserbycustomerid); [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | `IsActive` / `IsAccountLocked` set from one source field. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-020a Injured worker status from primary user type code

| Field | Value |
| --- | --- |
| Statement | An external user is an injured worker when the code of their primary user type is `WORK`, `GUARD`, or `POWA`. |
| Trigger | `GetExternalUser` and its callers. |
| Enforcement | `UserFactory.InjuredWorkerUserTypes`. |
| Origin | service |
| Reached from | [`GetExternalUser`](contracts/security-service.md#getexternaluser); [`GetExternalUserByCustomerId`](contracts/security-service.md#getexternaluserbycustomerid); [`GetInjuredWorkerUserByCustomerId`](contracts/security-service.md#getinjuredworkeruserbycustomerid); [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | `IsInjuredWorker` set; `PrimaryUserType` populated with description fields defaulted to `""`. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-021a Primary contact selection rules

| Field | Value |
| --- | --- |
| Statement | A person's primary contact is the first active primary address, email and phone, where a fax is the phone whose code is `F` and the primary phone is any active primary phone whose code is not `F`. |
| Trigger | `GetExternalUser` and its callers. |
| Enforcement | `ContactFactory.CreatePrimaryContact(Person)`. |
| Origin | service |
| Reached from | [`GetExternalUser`](contracts/security-service.md#getexternaluser); [`GetExternalUserByCustomerId`](contracts/security-service.md#getexternaluserbycustomerid); [`GetInjuredWorkerUserByCustomerId`](contracts/security-service.md#getinjuredworkeruserbycustomerid); [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | Contact populated from the selected rows. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/ContactFactory.cs` |

---

## BR-WCF-022a Missing employee resolves to an empty user id

| Field | Value |
| --- | --- |
| Statement | An employee id with no matching employee resolves to an empty user id, not null. |
| Trigger | `GetUserIdForEmployeeId`. |
| Enforcement | `UserFactory.GetUserIdForEmployeeId`. |
| Origin | service |
| Reached from | [`GetUserIdForEmployeeId`](contracts/security-service.md#getuseridforemployeeid) |
| Outcome | `""` returned. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-023a Employee id lookup falls back to the ADP projection

| Field | Value |
| --- | --- |
| Statement | Employee-id lookups match case-insensitively by upper-casing both sides, and a directory miss falls back to the ADP employee projection. |
| Trigger | `GetSaifUserByEmployeeId`. |
| Enforcement | `UserFactory.TryGetSaifUserByEmployeeId`. |
| Origin | service |
| Reached from | [`GetSaifUserByEmployeeId`](contracts/security-service.md#getsaifuserbyemployeeid) |
| Outcome | `SaifUser` from AD, else from ADP, else null. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-024a Manager user id comes from a second directory lookup

| Field | Value |
| --- | --- |
| Statement | A user's manager user id is resolved by a second directory lookup on the manager's common name, and is empty when no manager name is recorded. |
| Trigger | `GetSaifUserByUserId`. |
| Enforcement | `SaifUserPrincipal.ManagerName` / `.ManagerUserId`. |
| Origin | service |
| Reached from | [`GetSaifUserByUserId`](contracts/security-service.md#getsaifuserbyuserid); [`GetSaifUserByEmployeeId`](contracts/security-service.md#getsaifuserbyemployeeid) |
| Outcome | Manager account name, or `""`. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/SaifUserPrincipal.cs` |

---

## BR-WCF-025a SAIF domain user carries directory-only attributes

| Field | Value |
| --- | --- |
| Statement | A SAIF-domain user carries the directory-only attributes department, employee id, manager, job title, desk location and primary fax, which the database sources cannot supply. |
| Trigger | `GetSaifUserByUserId`. |
| Enforcement | `UserFactory.TryGetSaifUser`. |
| Origin | service |
| Reached from | [`GetSaifUserByUserId`](contracts/security-service.md#getsaifuserbyuserid); [`GetSaifUserByEmployeeId`](contracts/security-service.md#getsaifuserbyemployeeid) |
| Outcome | Enriched `SaifUser` returned. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-026a Internal user resolved from AD first

| Field | Value |
| --- | --- |
| Statement | An internal user is resolved from Active Directory first because only the directory supplies the lock state. |
| Trigger | `GetUser`. |
| Enforcement | `UserFactory.TryGetInternalUser`. |
| Origin | service |
| Reached from | [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | AD projection preferred over both database sources. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-027a WCIS employee active unless status is IN

| Field | Value |
| --- | --- |
| Statement | A WCIS employee is active unless their status code is `IN`; a missing status is treated as active. |
| Trigger | `GetUser`. |
| Enforcement | `UserFactory.TryGetWcisEmployeeUser`. |
| Origin | service |
| Reached from | [`GetUser`](contracts/security-service.md#getuser) |
| Outcome | `IsActive` set accordingly. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---

## BR-WCF-028a ADP employee active only when status is A

| Field | Value |
| --- | --- |
| Statement | An ADP employee is active only when their employee status code is exactly `A`. |
| Trigger | `GetUser`, `GetSaifUserByEmployeeId`. |
| Enforcement | `UserFactory.GetAdpUser`. |
| Origin | service |
| Reached from | [`GetUser`](contracts/security-service.md#getuser); [`GetSaifUserByEmployeeId`](contracts/security-service.md#getsaifuserbyemployeeid) |
| Outcome | `IsActive` set accordingly. |
| Source | `src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs` |

---


## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Faults and error pipeline](faults.md)
- [Authentication and authorization](authentication-authorization.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Data model](data-model.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources
- src/WCF/SAIF.Services.WCF.Host/Services/FileService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/PreferenceService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/WebContextService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/ImagingService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/FaxService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/Security/SecurityService.cs
- src/WCF/SAIF.Services.WCF.Host/Services/Security/Users/UserFactory.cs
- src/WCF/SAIF.Services.WCF.Host/Services/Security/AuthorizationCacheService.cs
- src/WCF/SAIF.Services.WCF.Host/Extensions/ErrorLogErrorHandler.cs
